/*
Copyright 2023 The Karmada Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package certs

import (
	"crypto"
	"crypto/ecdsa"
	"crypto/elliptic"
	cryptorand "crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"fmt"
	"math"
	"math/big"
	"net"
	"time"

	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/apimachinery/pkg/util/validation"
	certutil "k8s.io/client-go/util/cert"
	"k8s.io/client-go/util/keyutil"
	netutils "k8s.io/utils/net"

	operatorv1alpha1 "github.com/karmada-io/karmada/operator/pkg/apis/operator/v1alpha1"
	"github.com/karmada-io/karmada/operator/pkg/constants"
	"github.com/karmada-io/karmada/operator/pkg/util"
)

const (
	// CertificateBlockType is a possible value for pem.Block.Type.
	CertificateBlockType = "CERTIFICATE"
	rsaKeySize           = 3072
	keyExtension         = ".key"
	certExtension        = ".crt"
)

// AltNamesMutatorConfig is a config to AltNamesMutator. It includes necessary
// configs to AltNamesMutator.
type AltNamesMutatorConfig struct {
	Name                string
	Namespace           string
	ControlplaneAddress string
	Components          *operatorv1alpha1.KarmadaComponents
}

type altNamesMutatorFunc func(*AltNamesMutatorConfig, *CertConfig) error

// CertConfig represents a config to generate certificate by karmada.
type CertConfig struct {
	Name                string
	CAName              string
	NotAfter            *time.Time
	PublicKeyAlgorithm  x509.PublicKeyAlgorithm // TODO: All public key of karmada cert use the RSA algorithm by default
	Config              certutil.Config
	AltNamesMutatorFunc altNamesMutatorFunc
}

func (config *CertConfig) defaultPublicKeyAlgorithm() {
	if config.PublicKeyAlgorithm == x509.UnknownPublicKeyAlgorithm {
		config.PublicKeyAlgorithm = x509.RSA
	}
}

func (config *CertConfig) defaultNotAfter() {
	if config.NotAfter == nil {
		notAfter := time.Now().Add(constants.CertificateValidity).UTC()
		config.NotAfter = &notAfter
	}
}

// GetDefaultCertList returns all of karmada certConfigs, it include karmada, front and etcd.
func GetDefaultCertList(karmada *operatorv1alpha1.Karmada) []*CertConfig {
	certConfigs := []*CertConfig{
		// karmada cert config.
		KarmadaCertRootCA(),
		KarmadaCertAdmin(),
		KarmadaCertApiserver(),
		// front proxy cert config.
		KarmadaCertFrontProxyCA(),
		KarmadaCertFrontProxyClient(),
	}
	if karmada.Spec.Components.Etcd.Local != nil {
		certConfigs = append(certConfigs, KarmadaCertEtcdCA(), KarmadaCertEtcdServer(), KarmadaCertEtcdClient())
	}
	return certConfigs
}

// KarmadaCertRootCA returns karmada ca cert config.
func KarmadaCertRootCA() *CertConfig {
	return &CertConfig{
		Name: constants.CaCertAndKeyName,
		Config: certutil.Config{
			CommonName: "karmada",
		},
	}
}

// KarmadaCertAdmin returns karmada client cert config.
func KarmadaCertAdmin() *CertConfig {
	return &CertConfig{
		Name:   constants.KarmadaCertAndKeyName,
		CAName: constants.CaCertAndKeyName,
		Config: certutil.Config{
			CommonName:   "system:admin",
			Organization: []string{"system:masters"},
			Usages:       []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
		},
		AltNamesMutatorFunc: makeAltNamesMutator(apiServerAltNamesMutator),
	}
}

// KarmadaCertApiserver returns karmada apiserver cert config.
func KarmadaCertApiserver() *CertConfig {
	return &CertConfig{
		Name:   constants.ApiserverCertAndKeyName,
		CAName: constants.CaCertAndKeyName,
		Config: certutil.Config{
			CommonName: "karmada-apiserver",
			Usages:     []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		},
		AltNamesMutatorFunc: makeAltNamesMutator(apiServerAltNamesMutator),
	}
}

// KarmadaCertClient returns karmada client cert config.
func KarmadaCertClient() *CertConfig {
	return &CertConfig{
		Name:   "karmada-client",
		CAName: constants.CaCertAndKeyName,
		Config: certutil.Config{
			CommonName:   "system:admin",
			Organization: []string{"system:masters"},
			Usages:       []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
		},
		AltNamesMutatorFunc: makeAltNamesMutator(apiServerAltNamesMutator),
	}
}

// KarmadaCertFrontProxyCA returns karmada front proxy cert config.
func KarmadaCertFrontProxyCA() *CertConfig {
	return &CertConfig{
		Name: constants.FrontProxyCaCertAndKeyName,
		Config: certutil.Config{
			CommonName: "front-proxy-ca",
		},
	}
}

// KarmadaCertFrontProxyClient returns karmada front proxy client cert config.
func KarmadaCertFrontProxyClient() *CertConfig {
	return &CertConfig{
		Name:   constants.FrontProxyClientCertAndKeyName,
		CAName: constants.FrontProxyCaCertAndKeyName,
		Config: certutil.Config{
			CommonName: "front-proxy-client",
			Usages:     []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
		},
	}
}

// KarmadaCertEtcdCA returns karmada front proxy client cert config.
func KarmadaCertEtcdCA() *CertConfig {
	return &CertConfig{
		Name: constants.EtcdCaCertAndKeyName,
		Config: certutil.Config{
			CommonName: "karmada-etcd-ca",
		},
	}
}

// KarmadaCertEtcdServer returns etcd server cert config.
func KarmadaCertEtcdServer() *CertConfig {
	return &CertConfig{
		Name:   constants.EtcdServerCertAndKeyName,
		CAName: constants.EtcdCaCertAndKeyName,
		Config: certutil.Config{
			CommonName: "karmada-etcd-server",
			Usages:     []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
		},
		AltNamesMutatorFunc: makeAltNamesMutator(etcdServerAltNamesMutator),
	}
}

// KarmadaCertEtcdClient returns etcd client cert config.
func KarmadaCertEtcdClient() *CertConfig {
	return &CertConfig{
		Name:   constants.EtcdClientCertAndKeyName,
		CAName: constants.EtcdCaCertAndKeyName,
		Config: certutil.Config{
			CommonName: "karmada-etcd-client",
			Usages:     []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
		},
	}
}

// KarmadaCert is karmada certificate, it includes certificate basic message.
// we can directly get the byte array of certificate key and cert from the object.
type KarmadaCert struct {
	pairName string
	caName   string
	cert     []byte
	key      []byte
}

// NewKarmadaCert is used to create a new Karmada cert
func NewKarmadaCert(pairName, caName string, cert, key []byte) *KarmadaCert {
	return &KarmadaCert{pairName: pairName, caName: caName, cert: cert, key: key}
}

// CertData returns certificate cert data.
func (cert *KarmadaCert) CertData() []byte {
	return cert.cert
}

// KeyData returns certificate key data.
func (cert *KarmadaCert) KeyData() []byte {
	return cert.key
}

// CertName returns cert file name. its default suffix is ".crt".
func (cert *KarmadaCert) CertName() string {
	pair := cert.pairName
	if len(pair) == 0 {
		pair = "cert"
	}
	return pair + certExtension
}

// KeyName returns cert key file name. its default suffix is ".key".
func (cert *KarmadaCert) KeyName() string {
	pair := cert.pairName
	if len(pair) == 0 {
		pair = "cert"
	}
	return pair + keyExtension
}

// GeneratePrivateKey generates a certificate key. It supports both
// ECDSA (using the P-256 elliptic curve) and RSA algorithms. For RSA,
// the key is generated with a size of 3072 bits. If the keyType is
// x509.UnknownPublicKeyAlgorithm, the function defaults to generating
// an RSA key.
func GeneratePrivateKey(keyType x509.PublicKeyAlgorithm) (crypto.Signer, error) {
	switch keyType {
	case x509.ECDSA:
		return ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
	case x509.RSA, x509.UnknownPublicKeyAlgorithm:
		return rsa.GenerateKey(cryptorand.Reader, rsaKeySize)
	default:
		return nil, fmt.Errorf("unsupported key type: %T, supported key types are RSA and ECDSA", keyType)
	}
}

// NewCertificateAuthority creates new certificate and private key for the certificate authority
func NewCertificateAuthority(cc *CertConfig) (*KarmadaCert, error) {
	cc.defaultPublicKeyAlgorithm()

	key, err := GeneratePrivateKey(cc.PublicKeyAlgorithm)
	if err != nil {
		return nil, fmt.Errorf("unable to create private key while generating CA certificate, err: %w", err)
	}

	cert, err := certutil.NewSelfSignedCACert(cc.Config, key)
	if err != nil {
		return nil, fmt.Errorf("unable to create self-signed CA certificate, err: %w", err)
	}

	encoded, err := keyutil.MarshalPrivateKeyToPEM(key)
	if err != nil {
		return nil, fmt.Errorf("unable to marshal private key to PEM, err: %w", err)
	}

	return &KarmadaCert{
		pairName: cc.Name,
		caName:   cc.CAName,
		cert:     EncodeCertPEM(cert),
		key:      encoded,
	}, nil
}

// CreateCertAndKeyFilesWithCA loads the given certificate authority from disk, then generates and writes out the given certificate and key.
// The certSpec and caCertSpec should both be one of the variables from this package.
func CreateCertAndKeyFilesWithCA(cc *CertConfig, caCertData, caKeyData []byte) (*KarmadaCert, error) {
	if len(cc.Config.Usages) == 0 {
		return nil, fmt.Errorf("must specify at least one ExtKeyUsage")
	}

	cc.defaultNotAfter()
	cc.defaultPublicKeyAlgorithm()

	key, err := GeneratePrivateKey(cc.PublicKeyAlgorithm)
	if err != nil {
		return nil, fmt.Errorf("unable to create private key, err: %w", err)
	}

	caCerts, err := certutil.ParseCertsPEM(caCertData)
	if err != nil {
		return nil, err
	}

	caKey, err := ParsePrivateKeyPEM(caKeyData)
	if err != nil {
		return nil, err
	}

	// Safely pick the first one because the sender's certificate must come first in the list.
	// For details, see: https://www.rfc-editor.org/rfc/rfc4346#section-7.4.2
	caCert := caCerts[0]

	cert, err := NewSignedCert(cc, key, caCert, caKey, false)
	if err != nil {
		return nil, err
	}

	encoded, err := keyutil.MarshalPrivateKeyToPEM(key)
	if err != nil {
		return nil, fmt.Errorf("unable to marshal private key to PEM, err: %w", err)
	}

	return &KarmadaCert{
		pairName: cc.Name,
		caName:   cc.CAName,
		cert:     EncodeCertPEM(cert),
		key:      encoded,
	}, nil
}

// NewSignedCert creates a signed certificate using the given CA certificate and key
func NewSignedCert(cc *CertConfig, key crypto.Signer, caCert *x509.Certificate, caKey crypto.Signer, isCA bool) (*x509.Certificate, error) {
	serial, err := cryptorand.Int(cryptorand.Reader, new(big.Int).SetInt64(math.MaxInt64))
	if err != nil {
		return nil, err
	}
	if len(cc.Config.CommonName) == 0 {
		return nil, fmt.Errorf("must specify a CommonName")
	}

	keyUsage := x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature
	if isCA {
		keyUsage |= x509.KeyUsageCertSign
	}

	RemoveDuplicateAltNames(&cc.Config.AltNames)

	certTmpl := x509.Certificate{
		Subject: pkix.Name{
			CommonName:   cc.Config.CommonName,
			Organization: cc.Config.Organization,
		},
		DNSNames:              cc.Config.AltNames.DNSNames,
		IPAddresses:           cc.Config.AltNames.IPs,
		SerialNumber:          serial,
		NotBefore:             caCert.NotBefore,
		NotAfter:              cc.NotAfter.UTC(),
		KeyUsage:              keyUsage,
		ExtKeyUsage:           cc.Config.Usages,
		BasicConstraintsValid: true,
		IsCA:                  isCA,
	}
	certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &certTmpl, caCert, key.Public(), caKey)
	if err != nil {
		return nil, err
	}
	return x509.ParseCertificate(certDERBytes)
}

// RemoveDuplicateAltNames removes duplicate items in altNames.
func RemoveDuplicateAltNames(altNames *certutil.AltNames) {
	if altNames == nil {
		return
	}

	if altNames.DNSNames != nil {
		altNames.DNSNames = sets.NewString(altNames.DNSNames...).List()
	}

	ipsKeys := make(map[string]struct{})
	var ips []net.IP
	for _, one := range altNames.IPs {
		if _, ok := ipsKeys[one.String()]; !ok {
			ipsKeys[one.String()] = struct{}{}
			ips = append(ips, one)
		}
	}
	altNames.IPs = ips
}

func appendSANsToAltNames(altNames *certutil.AltNames, SANs []string) {
	for _, altname := range SANs {
		if ip := netutils.ParseIPSloppy(altname); ip != nil {
			altNames.IPs = append(altNames.IPs, ip)
		} else if len(validation.IsDNS1123Subdomain(altname)) == 0 {
			altNames.DNSNames = append(altNames.DNSNames, altname)
		} else if len(validation.IsWildcardDNS1123Subdomain(altname)) == 0 {
			altNames.DNSNames = append(altNames.DNSNames, altname)
		}
	}
}

// EncodeCertPEM returns PEM-encoded certificate data
func EncodeCertPEM(cert *x509.Certificate) []byte {
	block := pem.Block{
		Type:  CertificateBlockType,
		Bytes: cert.Raw,
	}
	return pem.EncodeToMemory(&block)
}

// ParsePrivateKeyPEM parses crypto.Signer from byte array. the key
// must be encryption by ECDSA and RAS.
func ParsePrivateKeyPEM(keyData []byte) (crypto.Signer, error) {
	caPrivateKey, err := keyutil.ParsePrivateKeyPEM(keyData)
	if err != nil {
		return nil, err
	}

	// Allow RSA and ECDSA formats only
	var key crypto.Signer
	switch k := caPrivateKey.(type) {
	case *rsa.PrivateKey:
		key = k
	case *ecdsa.PrivateKey:
		key = k
	default:
		return nil, fmt.Errorf("the private key is in an unsupported format: %s, supported formats are RSA and ECDSA", caPrivateKey)
	}

	return key, nil
}

func makeAltNamesMutator(f func(cfg *AltNamesMutatorConfig) (*certutil.AltNames, error)) altNamesMutatorFunc {
	return func(cfg *AltNamesMutatorConfig, cc *CertConfig) error {
		altNames, err := f(cfg)
		if err != nil {
			return err
		}

		cc.Config.AltNames = *altNames
		return nil
	}
}

func etcdServerAltNamesMutator(cfg *AltNamesMutatorConfig) (*certutil.AltNames, error) {
	etcdClientServiceDNS := fmt.Sprintf("%s.%s.svc.cluster.local", util.KarmadaEtcdClientName(cfg.Name), cfg.Namespace)
	etcdPeerServiceDNS := fmt.Sprintf("*.%s.%s.svc.cluster.local", util.KarmadaEtcdName(cfg.Name), cfg.Namespace)

	altNames := &certutil.AltNames{
		DNSNames: []string{"localhost", etcdClientServiceDNS, etcdPeerServiceDNS},
		IPs:      []net.IP{net.IPv4(127, 0, 0, 1)},
	}

	if cfg.Components != nil && cfg.Components.Etcd != nil && cfg.Components.Etcd.Local != nil {
		appendSANsToAltNames(altNames, cfg.Components.Etcd.Local.ServerCertSANs)
	}

	return altNames, nil
}

func apiServerAltNamesMutator(cfg *AltNamesMutatorConfig) (*certutil.AltNames, error) {
	altNames := &certutil.AltNames{
		DNSNames: []string{
			"localhost",
			"kubernetes",
			"kubernetes.default",
			"kubernetes.default.svc",
			fmt.Sprintf("*.%s.svc.cluster.local", constants.KarmadaSystemNamespace),
			fmt.Sprintf("*.%s.svc", constants.KarmadaSystemNamespace),
		},
		IPs: []net.IP{
			net.IPv4(127, 0, 0, 1),
		},
	}

	// When deploying a karmada under a namespace other than 'karmada-system', like 'test', there are two scenarios below：
	// 1.When karmada-apiserver access APIService, the cert of 'karmada-demo-aggregated-apiserver' will be verified to see
	// if its altNames contains 'karmada-demo-aggregated-apiserver.karmada-system.svc';
	// 2.When karmada-apiserver access webhook, the cert of 'karmada-demo-webhook' will be verified to see
	// if its altNames contains 'karmada-demo-webhook.test.svc'.
	// Therefore, the certificate's altNames should contain both 'karmada-system.svc.cluster.local' and 'test.svc.cluster.local'.
	if cfg.Namespace != constants.KarmadaSystemNamespace {
		appendSANsToAltNames(altNames, []string{fmt.Sprintf("*.%s.svc.cluster.local", cfg.Namespace),
			fmt.Sprintf("*.%s.svc", cfg.Namespace)})
	}

	if cfg.Components != nil && cfg.Components.KarmadaAPIServer != nil && len(cfg.Components.KarmadaAPIServer.CertSANs) > 0 {
		appendSANsToAltNames(altNames, cfg.Components.KarmadaAPIServer.CertSANs)
	}
	if len(cfg.ControlplaneAddress) > 0 {
		appendSANsToAltNames(altNames, []string{cfg.ControlplaneAddress})
	}

	return altNames, nil
}
